﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Serialization;
using System.IO;
using System.Text.RegularExpressions;

namespace UnrealDebugger
{
    [Serializable]
    public class ScriptOutline
    {
        public ScriptOutline()
        {
            this.Class = "";
            this.Parent = "";

            this.DefaultProperties = null;
            this.Replication = null;

            this.Functions = new List<FunctionDesc>();
            this.Events = new List<EventDesc>();
            this.Variables = new List<VarDesc>();
            this.States = new List<StateDesc>();
        }

        #region Script Contents

        [Serializable]
        public class ScriptDesc
        {
            public int Line { get; set; }
            public string Identifier { get; set; }
            public string Statement { get; set; }

            public ScriptDesc()
            {
                this.Line = 0;
                this.Identifier = "";
                this.Statement = "";
            }

            public ScriptDesc(int line, string identifier, string statement)
            {
                this.Line = line;
                this.Identifier = identifier;
                this.Statement = statement;
            }
        }

        [Serializable]
        public class DefaultPropertiesDesc : ScriptDesc
        {
            public DefaultPropertiesDesc()
            {
            }

            public DefaultPropertiesDesc(int line)
                : base(line, "defaultproperties", "")
            {
            }

            public override string ToString()
            {
                return Identifier + "@" + Line.ToString();
            }
        }

        [Serializable]
        public class ReplicationDesc : ScriptDesc
        {
            public ReplicationDesc()
            {
            }

            public ReplicationDesc(int line)
                : base(line, "replication", "")
            {
            }

            public override string ToString()
            {
                return Identifier + "@" + Line.ToString();
            }
        }

        [Serializable]
        public class StateDesc : ScriptDesc
        {
            public StateDesc()
            {
            }

            public StateDesc(int line, string identifier, string statement)
                : base(line, identifier, statement)
            {
            }

            public override string ToString()
            {
                return Identifier + "@" + Line.ToString();
            }
        }
        
        [Serializable]
        public class VarDesc : ScriptDesc
        {
            public VarDesc()
            {
            }

            public VarDesc(int line, string identifier, string statement)
                : base(line, identifier, statement)
            {
            }

            public override string ToString()
            {
                return Identifier + "@" + Line.ToString();
            }
        }

        [Serializable]
        public class FunctionDesc : ScriptDesc
        {
            public string State { get; set; }

            public FunctionDesc()
            {
                this.State = "";
            }

            public FunctionDesc(int line, string identifier, string statement, string state)
                : base(line, identifier, statement)
            {
                this.State = state;
            }

            public override string ToString()
            {
                return State + ":" + Identifier + "@" + Line.ToString();
            }
        }

        [Serializable]
        public class EventDesc : ScriptDesc
        {
            public string State { get; set; }

            public EventDesc()
            {
                this.State = "";
            }

            public EventDesc(int line, string identifier, string statement, string state)
                : base(line, identifier, statement)
            {
                this.State = state;
            }

            public override string ToString()
            {
                return State + ":" + Identifier + "@" + Line.ToString();
            }
        }

        public string Class { get; set; }
        public string Parent { get; set; }

        public DefaultPropertiesDesc DefaultProperties { get; set; }
        public ReplicationDesc Replication { get; set; }

        public List<VarDesc> Variables { get; set; }
        public List<FunctionDesc> Functions { get; set; }
        public List<EventDesc> Events { get; set; }
        public List<StateDesc> States { get; set; }

        #endregion

        #region Script Parsing

        public static ScriptOutline CreateFromScript(string file)
        {
            ScriptOutline outline = null;

            try
            {
                using (StreamReader reader = new StreamReader(file))
                {
                    string source = reader.ReadToEnd();
                    source = Uncomment(source);
                    outline = Analize(source);
                }
            }
            catch (Exception e)
            {
                Exception ex = new Exception("There was an error while parsing the script contents for file '" + file + "'. Script outline will not be available", e);
                ex.Source = e.Source;

                ExceptionMessageBox msgBox = new ExceptionMessageBox(ex);
                msgBox.ShowDialog();

                return new ScriptOutline();
            }

            return outline;
        }

        #region Helper functions for script parsing

        private enum CommentType 
        { 
            None,
            String,
            SingleLine,
            MultiLine, 
        }

        private enum Keywords
        {
            Var,
            Function,
            Event,
            State,
            Class,
            DefaultProperties,
            Replication,
            CppText,
        }

        private static char[] _blockSeparators = null;
        private static char[] _statementSeparators = null;
        private static Regex _regexKeyords = null;
        private static Regex _regexStatement = null;
        private static Regex _regexVarName = null;
        private static Regex _regexFunctionName = null;
        private static Regex _regexEventName = null;
        private static Regex _regexClassName = null;
        private static Regex _regexParentName = null;
        private static Regex _regexStateName = null;

        static ScriptOutline()
        {
            string pattern = "";
            string[] keywords = Enum.GetNames(typeof(Keywords));
            
            foreach (string keyword in keywords)
            {
                if (pattern.Length > 0) { pattern += "|"; }
                pattern += keyword;
            }

            _blockSeparators = new char[] { '{', '}' };
            _statementSeparators = new char[] { '{', '}', ';' };
            _regexKeyords = new Regex(@"\b(" + pattern + @")\b", RegexOptions.IgnoreCase | RegexOptions.Compiled);
            _regexStatement = new Regex(@"\s+", RegexOptions.IgnoreCase | RegexOptions.Compiled);
            _regexVarName = new Regex(@"\w+(?=\s*(\[[^\]]\])?\s*(\<[^\>].*\>\s*)?$)", RegexOptions.RightToLeft | RegexOptions.IgnoreCase | RegexOptions.Compiled);
            _regexFunctionName = new Regex(@"\w+(?=\s*\([^\)]*\).*$)", RegexOptions.RightToLeft | RegexOptions.IgnoreCase | RegexOptions.Compiled);
            _regexEventName = new Regex(@"\w+(?=\s*\([^\)]*\).*$)", RegexOptions.RightToLeft | RegexOptions.IgnoreCase | RegexOptions.Compiled);
            _regexClassName = new Regex(@"(?<=\sclass\s)\w+", RegexOptions.IgnoreCase | RegexOptions.Compiled);
            _regexParentName = new Regex(@"(?<=\sextends\s)\w+", RegexOptions.IgnoreCase | RegexOptions.Compiled);
            _regexStateName = new Regex(@"(?<=\sstate\s)\w+", RegexOptions.RightToLeft | RegexOptions.IgnoreCase | RegexOptions.Compiled);
        }

        private static string Uncomment(string source)
        {
            StringWriter writer = new StringWriter();
            StringReader reader = new StringReader(source);

            string line = null;
            CommentType type = CommentType.None;

            int lineNumber = 0;

            // Parse each line of the source code
            while ((line = reader.ReadLine()) != null)
            {
                lineNumber++;

                // Parse the current line
                while (line.Length > 0)
                {
                    int pos = line.Length;
                    string buffer = line;

                    switch (type)
                    {
                        // We are not inside a comment or a string
                        case CommentType.None:
                            {
                                // Check if the buffer contains a string
                                int stringPos = buffer.IndexOf('\x22');
                                if (stringPos >= 0)
                                {
                                    pos = stringPos + 1;
                                    buffer = stringPos > 0 ? line.Substring(0, stringPos) : "";
                                    type = CommentType.String;
                                }

                                // Check if the buffer contains a single line comment
                                int singleLinePos = buffer.IndexOf("//");
                                if (singleLinePos >= 0)
                                {
                                    pos = singleLinePos + 2;
                                    buffer = singleLinePos > 0 ? line.Substring(0, singleLinePos) : "";
                                    type = CommentType.SingleLine;
                                }

                                // Check if the buffer contains a multi line comment
                                int multiLinePos = buffer.IndexOf("/*");
                                if (multiLinePos >= 0)
                                {
                                    pos = multiLinePos + 2;
                                    buffer = multiLinePos > 0 ? line.Substring(0, multiLinePos) : "";
                                    type = CommentType.MultiLine;
                                }

                            } break;

                        // We are parsing a string
                        case CommentType.String:
                            {
                                // Check if the buffer has a string delimiter
                                int stringPos = buffer.IndexOf('\x22');
                                if (stringPos >= 0)
                                {
                                    pos = stringPos + 1;
                                    buffer = stringPos > 0 ? line.Substring(0, stringPos) : "";
                                    type = stringPos > 0 && line[stringPos - 1] == '\\' ? CommentType.String : CommentType.None;
                                }
                            } break;

                        // We are parsing a single line comment
                        case CommentType.SingleLine:
                            {
                                buffer = "";
                                type = CommentType.None;
                            } break;

                        // We are parsing a multiple line comment
                        case CommentType.MultiLine:
                            {
                                // Check if the buffer has a multi line comment delimiter
                                int multiLinePos = buffer.IndexOf("*/");
                                if (multiLinePos >= 0)
                                {
                                    pos = multiLinePos + 2;
                                    type = CommentType.None;
                                }

                                buffer = "";
                            } break;
                    }

                    // Advance the position in current line and copy the buffered text to the output string 
                    line = line.Substring(pos);
                    writer.Write(buffer);
                }

                if (type == CommentType.SingleLine) { type = CommentType.None; }
                writer.WriteLine();
            }

            reader.Close();
            return writer.ToString();
        }

        private static int FindKeyword(string source, int startPos, int endPos, ref Keywords keyword)
        {
            Match match = _regexKeyords.Match(source, startPos, endPos - startPos);
            if (match.Success)
            {
                Enum.TryParse<Keywords>(match.Value, true, out keyword);
                return match.Index;
            }

            return 0;
        }

        private static int ReadLine(string source, int startPos)
        {
            int endPos = source.IndexOf('\n', startPos);
            return endPos >= 0 ? endPos : source.Length;
        }

        private static bool ReadStatement(string source, int currentPos, ref int startPos, ref int endPos)
        {
            startPos = currentPos;

            while (startPos > 0 && !_statementSeparators.Contains(source[startPos - 1])) { startPos--; }

            endPos = currentPos;
            while (endPos < source.Length && !_statementSeparators.Contains(source[endPos])) { endPos++; }

            return endPos - startPos > 0;
        }

        private static ScriptOutline Analize(string source)
        {
            ScriptOutline outline = new ScriptOutline();
            StringReader reader = new StringReader(source);

            string state = "";
            bool cpptext = false;

            int lineNumber = 0;
            int depth = 0;

            int startPos = 0;
            int endPos = 0;

            // Parse each line of the source code
            while ((endPos = ReadLine(source, startPos)) < source.Length)
            {
                lineNumber++;

                // Parse the current line
                while (startPos < endPos)
                {
                    // Check if the line opens or closes a block
                    int blockPos = source.IndexOfAny(_blockSeparators, startPos, endPos - startPos);
                    if (blockPos >= 0)
                    {
                        startPos = blockPos + 1;
                        depth += source[blockPos] == '{' ? 1 : -1;
                        if (depth == 0) 
                        { 
                            cpptext = false;
                            state = "";
                        }
                    }

                    // If we are inside a cpptext declaration we ignore all code
                    // If we are outside a state declaration we ignore code with block depth > 0
                    // Otherwise we ignore code with block depth > 1
                    bool ignoreCode = cpptext;
                    ignoreCode |= depth > 0 && state.Length == 0;
                    ignoreCode |= depth > 1;

                    // If we don't ignore the code, we look for a keyword
                    if (!ignoreCode)
                    {
                        Keywords keyword = (Keywords) 0;
                        int keywordPos = FindKeyword(source, startPos, endPos, ref keyword);

                        if (keywordPos > 0)
                        {
                            // Read the full statement (up until ';' or '(')
                            int statementStartPos = 0;
                            int statementEndPos = 0;
                            ReadStatement(source, keywordPos, ref statementStartPos, ref statementEndPos);

                            // Remove unwanted characters from statement
                            string statement = _regexStatement.Replace(source.Substring(statementStartPos, statementEndPos - statementStartPos), " ");

                            // Extract relevant information from the statement and store it in the outline object
                            switch (keyword)
                            {
                                case Keywords.Class:
                                    {
                                        if (outline.Class.Length == 0)
                                        {
                                            outline.Class = _regexClassName.Match(statement).Value;
                                            outline.Parent = _regexParentName.Match(statement).Value;
                                        }
                                    } break;

                                case Keywords.Var:
                                    {
                                        string identifier = _regexVarName.Match(statement).Value;
                                        outline.Variables.Add(new VarDesc(lineNumber, identifier, statement));
                                    } break;

                                case Keywords.Function:
                                    {
                                        string identifier = _regexFunctionName.Match(statement).Value;
                                        outline.Functions.Add(new FunctionDesc(lineNumber, identifier, statement, state.Length > 0 ? state : "\tGlobal"));
                                    } break;

                                case Keywords.Event:
                                    {
                                        string identifier = _regexEventName.Match(statement).Value;
                                        outline.Events.Add(new EventDesc(lineNumber, identifier, statement, state.Length > 0 ? state : "\tGlobal"));
                                    } break;

                                case Keywords.State:
                                    {
                                        string identifier = _regexStateName.Match(statement).Value;
                                        outline.States.Add(new StateDesc(lineNumber, identifier, statement));
                                        state = identifier;
                                    } break;

                                case Keywords.DefaultProperties:
                                    {
                                        outline.DefaultProperties = new DefaultPropertiesDesc(lineNumber);
                                    } break;

                                case Keywords.Replication:
                                    {
                                        outline.Replication = new ReplicationDesc(lineNumber);
                                    } break;

                                case Keywords.CppText:
                                    {
                                        cpptext = true;
                                    } break;
                            }
                        }
                    }

                    // Advance the position in current line
                    startPos = blockPos >= 0 ? blockPos + 1 : endPos + 1;
                }
            }
            
            // Sort the contents alphabetically
            outline.Variables.Sort(delegate(VarDesc a, VarDesc b) { return string.Compare(a.Identifier, b.Identifier, true); });
            outline.Functions.Sort(delegate(FunctionDesc a, FunctionDesc b) { return string.Compare(a.Identifier, b.Identifier, true); });
            outline.Events.Sort(delegate(EventDesc a, EventDesc b) { return string.Compare(a.Identifier, b.Identifier, true); });
            outline.States.Sort(delegate(StateDesc a, StateDesc b) { return string.Compare(a.Identifier, b.Identifier, true); });

            return outline;
        }

        #endregion

        #endregion

        #region Serialization

        public bool SaveToXml(string file)
        {
            try
            {
                XmlSerializer xml = new XmlSerializer(typeof(ScriptOutline));

                using (TextWriter fs = new StreamWriter(file))
                {
                    xml.Serialize(fs, this);
                }

                return true;
            }
            catch (Exception e)
            {
                ExceptionMessageBox msgBox = new ExceptionMessageBox(e);
                msgBox.ShowDialog();
                return false;
            }
        }

        public static ScriptOutline LoadFromXml(string file)
        {
            try
            {
                ScriptOutline outline = null;
                XmlSerializer xml = new XmlSerializer(typeof(ScriptOutline));

                using (TextReader fs = new StreamReader(file))
                {
                    outline = xml.Deserialize(fs) as ScriptOutline;
                }

                return outline;
            }
            catch (Exception e)
            {
                ExceptionMessageBox msgBox = new ExceptionMessageBox(e);
                msgBox.ShowDialog();

                return new ScriptOutline();
            }
        }

        #endregion
    }
}
